/* router.h
   Mathieu Stefani, 05 janvier 2016

   Simple HTTP Rest Router
*/

#pragma once

#include <memory>
#include <regex>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>

#include <pistache/flags.h>
#include <pistache/http.h>
#include <pistache/http_defs.h>

#include "pistache/string_view.h"

namespace Pistache
{
    namespace Rest
    {

        class Description;

        namespace details
        {
            template <typename T>
            struct LexicalCast
            {
                static T cast(const std::string& value)
                {
                    std::istringstream iss(value);
                    T out;
                    if (!(iss >> out))
                        throw std::runtime_error("Bad lexical cast");
                    return out;
                }
            };

            template <>
            struct LexicalCast<std::string>
            {
                static std::string cast(const std::string& value) { return value; }
            };
        } // namespace details

        class TypedParam
        {
        public:
            TypedParam(std::string name, std::string value)
                : name_(std::move(name))
                , value_(std::move(value))
            { }

            template <typename T>
            T as() const
            {
                return details::LexicalCast<T>::cast(value_);
            }

            const std::string& name() const { return name_; }

        private:
            const std::string name_;
            const std::string value_;
        };

        class Request;

        struct Route
        {
            enum class Result { Ok,
                                Failure };

            enum class Status { Match,
                                NotFound,
                                NotAllowed };

            typedef std::function<Result(const Request, Http::ResponseWriter)> Handler;

            typedef std::function<bool(Http::Request& req, Http::ResponseWriter& resp)> Middleware;

            typedef std::function<void(const std::shared_ptr<Tcp::Peer>& peer)>
                DisconnectHandler;

            explicit Route(Route::Handler handler)
                : handler_(std::move(handler))
            { }

            template <typename... Args>
            void invokeHandler(Args&&... args) const
            {
                handler_(std::forward<Args>(args)...);
            }

            Handler handler_;
        };

        namespace Private
        {
            class RouterHandler;
        }

        /**
 * A request URI is made of various path segments.
 * Since all routes handled by a router are naturally
 * represented as a tree, this class provides support for it.
 * It is possible to perform tree-based routing search instead
 * of linear one.
 * This class holds all data for a given path segment, meaning
 * that it holds the associated route handler (if any) and all
 * next child routes (by means of fixed routes, parametric,
 * optional parametric and splats).
 * Each child is in turn a SegmentTreeNode.
 */
        class SegmentTreeNode
        {
        private:
            enum class SegmentType { Fixed,
                                     Param,
                                     Optional,
                                     Splat };

            /**
   * string_view are very efficient when working with the
   * substring function (massively used for routing) but are
   * non-owning. To let the content survive after it is firstly
   * created, their content is stored in this reference pointer
   * that is in charge of managing their lifecycle (create on add,
   * reset on remove).
   */
            std::shared_ptr<char> resource_ref_;

            std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> fixed_;
            std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>> param_;
            std::unordered_map<std::string_view, std::shared_ptr<SegmentTreeNode>>
                optional_;
            std::shared_ptr<SegmentTreeNode> splat_;
            std::shared_ptr<Route> route_;

            /**
   * Common web servers (nginx, httpd, IIS) collapse multiple
   * forward slashes to a single one. This regex is used to
   * obtain the same result.
   */
            static std::regex multiple_slash;

            static SegmentType getSegmentType(const std::string_view& fragment);

            /**
   * Fetches the route associated to a given path.
   * \param[in] path Requested resource path. Must have no leading slash
   * and no multiple slashes:
   * eg:
   * - auth/login is valid
   * - /auth/login is invalid
   * - auth//login is invalid
   * \param[in,out] params Contains all the parameters parsed so far.
   * \param[in,out] splats Contains all the splats parsed so far.
   * \returns Tuple containing the route, the list of all parsed parameters
   * and the list of all parsed splats.
   * \throws std::runtime_error An empty path was given
   */
            std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
                       std::vector<TypedParam>>
            findRoute(const std::string_view& path, std::vector<TypedParam>& params,
                      std::vector<TypedParam>& splats) const;

        public:
            SegmentTreeNode();
            explicit SegmentTreeNode(const std::shared_ptr<char>& resourceReference);

            /**
   * Sanitizes a resource URL by removing any duplicate slash, leading
   * slash and trailing slash.
   * @param path URL to sanitize.
   * @return Sanitized URL.
   */
            static std::string sanitizeResource(const std::string& path);

            /**
   * Associates a route handler to a given path.
   * \param[in] path Requested resource path. Must have no leading and trailing
   * slashes and no multiple slashes:
   * eg:
   * - auth/login is valid
   * - /auth/login is invalid
   * - auth/login/ is invalid
   * - auth//login is invalid
   * \param[in] handler Handler to associate to path.
   * \param[in] resource_reference \see SegmentTreeNode::resource_ref_
   * \throws std::runtime_error An empty path was given
   */
            void addRoute(const std::string_view& path, const Route::Handler& handler,
                          const std::shared_ptr<char>& resource_reference);

            /**
   * Removes the route handler associated to a given path.
   * \param[in] path Requested resource path. Must have no leading slash
   * and no multiple slashes:
   * eg:
   * - auth/login is valid
   * - /auth/login is invalid
   * - auth//login is invalid
   * \throws std::runtime_error An empty path was given
   */
            bool removeRoute(const std::string_view& path);

            /**
   * Finds the correct route for the given path.
   * \param[in] path Requested resource path. Must have no leading slash
   * and no multiple slashes:
   * eg:
   * - auth/login is valid
   * - /auth/login is invalid
   * - auth//login is invalid
   * \throws std::runtime_error An empty path was given
   * \return Found route with its resolved parameters and splats (if no route
   * is found, first element of the tuple is a null pointer).
   */
            std::tuple<std::shared_ptr<Route>, std::vector<TypedParam>,
                       std::vector<TypedParam>>
            findRoute(const std::string_view& path) const;
        };

        class Router
        {
        public:
            static Router fromDescription(const Rest::Description& desc);

            std::shared_ptr<Private::RouterHandler> handler() const;
            static std::shared_ptr<Private::RouterHandler>
            handler(std::shared_ptr<Rest::Router> router);

            void initFromDescription(const Rest::Description& desc);

            void get(const std::string& resource, Route::Handler handler);
            void post(const std::string& resource, Route::Handler handler);
            void put(const std::string& resource, Route::Handler handler);
            void patch(const std::string& resource, Route::Handler handler);
            void del(const std::string& resource, Route::Handler handler);
            void options(const std::string& resource, Route::Handler handler);
            void addRoute(Http::Method method, const std::string& resource,
                          Route::Handler handler);
            void removeRoute(Http::Method method, const std::string& resource);
            void head(const std::string& resource, Route::Handler handler);

            void addCustomHandler(Route::Handler handler);
            void addMiddleware(Route::Middleware middleware);

            void addNotFoundHandler(Route::Handler handler);
            void addDisconnectHandler(Route::DisconnectHandler handler);
            inline bool hasNotFoundHandler() { return notFoundHandler != nullptr; }
            void invokeNotFoundHandler(const Http::Request& req,
                                       Http::ResponseWriter resp) const;

            void disconnectPeer(const std::shared_ptr<Tcp::Peer>& peer);

            Route::Status route(const Http::Request& request,
                                Http::ResponseWriter response);

            Router()
                : routes()
                , customHandlers()
                , middlewares()
                , notFoundHandler()
            { }

        private:
            std::unordered_map<Http::Method, SegmentTreeNode> routes;

            std::vector<Route::Handler> customHandlers;

            std::vector<Route::Middleware> middlewares;

            std::vector<Route::DisconnectHandler> disconnectHandlers;

            Route::Handler notFoundHandler;
        };

        namespace Private
        {

            class RouterHandler : public Http::Handler
            {
            public:
                HTTP_PROTOTYPE(RouterHandler)

                /**
   * Used for immutable router. Useful if all the routes are
   * defined at compile time (and for backward compatibility)
   * \param[in] router Immutable router.
   */
                explicit RouterHandler(const Rest::Router& router);

                /**
   * Used for mutable router. Useful if it is required to
   * add/remove routes at runtime.
   * \param[in] router Pointer to a (mutable) router.
   */
                explicit RouterHandler(std::shared_ptr<Rest::Router> router);

                void onRequest(const Http::Request& req,
                               Http::ResponseWriter response) override;

                void onDisconnection(const std::shared_ptr<Tcp::Peer>& peer) override;

            private:
                std::shared_ptr<Rest::Router> router;
            };
        } // namespace Private

        class Request : public Http::Request
        {
        public:
            friend class Router;

            bool hasParam(const std::string& name) const;
            TypedParam param(const std::string& name) const;

            TypedParam splatAt(size_t index) const;
            std::vector<TypedParam> splat() const;

        private:
            explicit Request(Http::Request request,
                             std::vector<TypedParam>&& params,
                             std::vector<TypedParam>&& splats);

            std::vector<TypedParam> params_;
            std::vector<TypedParam> splats_;
        };

        namespace Routes
        {

            void Get(Router& router, const std::string& resource, Route::Handler handler);
            void Post(Router& router, const std::string& resource, Route::Handler handler);
            void Put(Router& router, const std::string& resource, Route::Handler handler);
            void Patch(Router& router, const std::string& resource, Route::Handler handler);
            void Delete(Router& router, const std::string& resource,
                        Route::Handler handler);
            void Options(Router& router, const std::string& resource,
                         Route::Handler handler);
            void Remove(Router& router, Http::Method method, const std::string& resource);
            void Head(Router& router, const std::string& resource, Route::Handler handler);

            void NotFound(Router& router, Route::Handler handler);

            namespace details
            {
                template <typename... Args>
                struct TypeList
                {
                    template <size_t N>
                    struct At
                    {
                        static_assert(N < sizeof...(Args), "Invalid index");

                        using Type = typename std::tuple_element<N, std::tuple<Args...>>::type;
                    };
                };

                template <typename Request, typename Response>
                struct BindChecks
                {
                    constexpr static bool request_check = std::is_const<typename std::remove_reference<Request>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Request>::type>::value && std::is_same<typename std::decay<Request>::type, Rest::Request>::value;

                    constexpr static bool response_check = !std::is_const<typename std::remove_reference<Response>::type>::value && std::is_same<typename std::remove_reference<Response>::type, Response>::value && std::is_same<typename std::decay<Response>::type, Http::ResponseWriter>::value;

                    static_assert(
                        request_check && response_check,
                        "Function should accept (const Rest::Request&, HttpResponseWriter)");
                };

                template <typename Request, typename Response>
                struct MiddlewareChecks
                {
                    constexpr static bool request_check = !std::is_const<typename std::remove_reference<Request>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Request>::type>::value && std::is_same<typename std::decay<Request>::type, Http::Request>::value;

                    constexpr static bool response_check = !std::is_const<typename std::remove_reference<Response>::type>::value && std::is_lvalue_reference<typename std::remove_cv<Response>::type>::value && std::is_same<typename std::decay<Response>::type, Http::ResponseWriter>::value;

                    static_assert(request_check && response_check,
                                  "Function should accept (Http::Request&, HttpResponseWriter&)");
                };

                template <template <typename, typename> class Checks, typename... Args>
                constexpr void static_checks()
                {
                    static_assert(sizeof...(Args) == 2, "Function should take 2 parameters");

                    using Arguments = details::TypeList<Args...>;

                    using Request  = typename Arguments::template At<0>::Type;
                    using Response = typename Arguments::template At<1>::Type;

                    // instantiate template this way
                    constexpr Checks<Request, Response> checks;

                    UNUSED(checks);
                }
            } // namespace details

            template <typename Result, typename Cls, typename... Args, typename Obj>
            Route::Handler bind(Result (Cls::*func)(Args...), Obj obj)
            {
                details::static_checks<details::BindChecks, Args...>();

                return [=](const Rest::Request& request, Http::ResponseWriter response) {
                    (obj->*func)(request, std::move(response));

                    return Route::Result::Ok;
                };
            }

            template <typename Result, typename Cls, typename... Args, typename Obj>
            Route::Handler bind(Result (Cls::*func)(Args...), std::shared_ptr<Obj> objPtr)
            {
                details::static_checks<details::BindChecks, Args...>();

                return [=](const Rest::Request& request, Http::ResponseWriter response) {
                    (objPtr.get()->*func)(request, std::move(response));

                    return Route::Result::Ok;
                };
            }

            template <typename Result, typename... Args>
            Route::Handler bind(Result (*func)(Args...))
            {
                details::static_checks<details::BindChecks, Args...>();

                return [=](const Rest::Request& request, Http::ResponseWriter response) {
                    func(request, std::move(response));

                    return Route::Result::Ok;
                };
            }

            template <typename Cls, typename... Args, typename Obj>
            Route::Middleware middleware(bool (Cls::*func)(Args...), Obj obj)
            {
                details::static_checks<details::MiddlewareChecks, Args...>();

                return [=](Http::Request& request, Http::ResponseWriter& response) {
                    return (obj->*func)(request, response);
                };
            }

            template <typename Cls, typename... Args, typename Obj>
            Route::Middleware middleware(bool (Cls::*func)(Args...),
                                         std::shared_ptr<Obj> objPtr)
            {
                details::static_checks<details::MiddlewareChecks, Args...>();

                return [=](Http::Request& request, Http::ResponseWriter& response) {
                    return (objPtr.get()->*func)(request, response);
                };
            }

            template <typename... Args>
            Route::Middleware middleware(bool (*func)(Args...))
            {
                details::static_checks<details::MiddlewareChecks, Args...>();

                return [=](Http::Request& request, Http::ResponseWriter& response) {
                    return func(request, response);
                };
            }

        } // namespace Routes
    } // namespace Rest
} // namespace Pistache
